﻿using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
using Microsoft.VisualStudio.Text;
using Microsoft.VisualStudio.Text.Classification;
using Red.Scripts.Defines.Utilities;

namespace Language.Syntax.Colouring
{
    internal class Preclassifier
    {
        #region Private Types

        private class LocalTokens
        {
            public ITextVersion Version { get; set; }
            public SortedList<int, ResolvedClassification> Tokens { get; private set; } = new SortedList<int, ResolvedClassification>();
        }

        #endregion Private Types

        #region Private Data

        private ClassificationTypes _classificationTypes;

        private Scc.Connection _connection;
        private Stub.Database _database;
        private ITextBuffer _textBuffer;

        private DatabaseClassifier _databaseClassifier;

        private Parser.ChangeMonitor _miniParser;

        private LocalTokens _localTokens;

        #endregion Private Data

        #region C-Tor

        public Preclassifier( Scc.Connection connection, Stub.Database database, ITextBuffer textBuffer, IClassificationTypeRegistryService registry )
        {
            _connection = connection;
            _database = database;
            _textBuffer = textBuffer;

            _classificationTypes = new ClassificationTypes( registry );
            _databaseClassifier = new DatabaseClassifier( connection, database, _classificationTypes, textBuffer );

            _databaseClassifier.NewSnapshotAvailable += _databaseClassifier_NewSnapshotAvailable;

            _miniParser = new Parser.ChangeMonitor( database );
            _miniParser.Attach( _textBuffer );
            _miniParser.ParseComplete += _miniParser_ParseComplete;
        }

        #endregion C-Tor

        #region Public Methods

        public void AddClassification( ITextSnapshot snapshot, List<ClassificationSpan> destination, ResolvedClassification source, SortedList<int, ResolvedClassification> excluded )
        {
            int excludeIndex = excluded?.LowerBound( source.Span.GetStartPoint( snapshot ) ) ?? -1;

            if ( excludeIndex != -1 )
            {
                while ( excludeIndex < excluded.Values.Count )
                {
                    ResolvedClassification test = excluded.Values[ excludeIndex ];

                    if ( test.Span.GetSpan( snapshot ).IntersectsWith( source.Span.GetSpan( snapshot ) ) )
                        return;

                    if ( test.Span.GetStartPoint( snapshot ) > source.Span.GetEndPoint( snapshot ) )
                        break;

                    ++excludeIndex;
                }
            }

            destination.Add( source.GetClassification( snapshot ) );
        }

        public void AddClassifications( SnapshotSpan span, List<ClassificationSpan> destination, SortedList<int, ResolvedClassification> source, SortedList<int, ResolvedClassification> excluded )
        {
            int index = source.LowerBound( span.Start.Position );

            if ( index == -1 )
                return;

            while ( index < source.Values.Count && source.Values[ index ].Span.GetStartPoint( span.Snapshot ) < span.End.Position )
            {
                AddClassification( span.Snapshot, destination, source.Values[ index ], excluded );
                
                ++index;
            }
        }

        public IList<ClassificationSpan> GetClassificationSpans( SnapshotSpan span )
        {
            List<ClassificationSpan> classifications = new List<ClassificationSpan>();

            if( _localTokens  != null )
                AddClassifications( span, classifications, _localTokens.Tokens, null );

            if( _databaseClassifier.MostUpToDateSnapshot != null )
                AddClassifications( span, classifications, _databaseClassifier[ _databaseClassifier.MostUpToDateSnapshot ], _localTokens?.Tokens );

            return classifications;
        }

        #endregion Public Methods

        #region Private Methods

        SortedList<int, ResolvedClassification> ResolveClassifications( Parser.Results results, SnapshotSpan span, string relativeFilePath )
        {
            ClassificationResolver resolver = new ClassificationResolver( relativeFilePath, _database, _classificationTypes );

            Debug.Print( $"[Preclassifier] Task:{Task.CurrentId:#####} Thread:{System.Threading.Thread.CurrentThread.ManagedThreadId:#####} Resolving: {span}" );

            resolver.Scan( span.Snapshot, results.Tokens.Values, results.Stubs.Values, results.Literals.Values );

            return resolver.Classifications;
        }

        #endregion Private Methods

        #region Event Handlers

        private void _miniParser_ParseComplete( Parser.Results results, SnapshotSpan span, string relativeFilePath )
        {
            Debug.Assert( relativeFilePath == _databaseClassifier.RelativeFilePath );

            var resolvedClassifications = ResolveClassifications( results, span, relativeFilePath );

            ITextVersion newVersion = span.Snapshot.Version;

            // Erase the existing locally parsed tokens if this latest parse is for a more recent version
            // Or just create a new LocalTokens object if none exists
            if ( _localTokens?.Version.VersionNumber >= newVersion.VersionNumber == false )
            {
                _localTokens = new LocalTokens()
                {
                    Version = newVersion
                };
            }

            foreach ( var resolvedClassification in resolvedClassifications )
            {
                _localTokens.Tokens[ resolvedClassification.Key ] = resolvedClassification.Value;
            }

            System.Windows.Application.Current.Dispatcher.BeginInvoke( (Action)( () =>
            {
                NewSnapshotAvailable?.Invoke( span );
            } ), System.Windows.Threading.DispatcherPriority.Background );
        }

        private void _databaseClassifier_NewSnapshotAvailable( ITextSnapshot snapshot )
        {
            // Erase the locally parsed tokens if the database is the same version
            if( snapshot.Version.VersionNumber >= _localTokens?.Version.VersionNumber == true )
                _localTokens = null;

            NewSnapshotAvailable?.Invoke( new SnapshotSpan( snapshot, new Span( 0, snapshot.Length ) ) );
        }

        #endregion Event Handlers

        #region Events

        public delegate void NewSnapshotAvailableHandler( SnapshotSpan span );
        public event NewSnapshotAvailableHandler NewSnapshotAvailable;

        #endregion Events
    }
}
